package com.googlecode.sparda.commons.io.stream;


import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;

import com.googlecode.sparda.commons.io.stream.policy.CloseOldestOpenedStreamCMOSPolicy;
import com.googlecode.sparda.commons.io.stream.policy.ClosingManagedOutputStreamPolicy;

/**
 * This class is an {@link OutputStreamManager}, it manage the {@link ManagedOutputStream}.
 * This class is typically used when there are too many stream to open and we can't.
 * This class can manage an arbitrary number of stream, preserving the number of opened stream 
 * under the limit. 
 * @author Fabio Grucci (<i>alias</i> <b>Phoenix</b>)
 * @version 0.1
 * @since 0.1
 */
public class OutputStreamManager {
	/**
	 * The default number of allowed of opened stream.
	 */
	public static final int DEFAULT_MAX_NUMBER_OF_ALLOWED_OPEN_STREAM = 128;
	/**
	 * The number of allowed of opened stream.
	 */
	private int maxNumberOfAllowedOpenStream;
	/**
	 * The number of the currently opened stream.
	 */
	private int openStreamCount;
	/**
	 * The closing stream policy.
	 */
	private final ClosingManagedOutputStreamPolicy closingPolicy;
	/**
	 * The {@link Set} of not definitivelly closed stream.
	 */
	private final Set<ManagedOutputStream<? extends OutputStream>> notDefinitelyCloseStream;
	
	/**
	 * Build an {@link OutputStreamManager} whit a default policy and a default 
	 * number of max allowed opened stream. 
	 */
	public OutputStreamManager() {
		this(DEFAULT_MAX_NUMBER_OF_ALLOWED_OPEN_STREAM);
	}
	/**
	 * Build an {@link OutputStreamManager} whit a default policy.
	 * @param maxNumberOfAllowedOpenStream The max number of stream opened at same time
	 */
	public OutputStreamManager(int maxNumberOfAllowedOpenStream) {
		this(maxNumberOfAllowedOpenStream, new CloseOldestOpenedStreamCMOSPolicy());
	}
	
	/**
	 * Build an {@link OutputStreamManager}.
	 * @param maxNumberOfAllowedOpenStream The max number of stream opened at same time
	 * @param closingPolicy The policy to choose the stream to closed
	 */
	public OutputStreamManager(int maxNumberOfAllowedOpenStream, ClosingManagedOutputStreamPolicy closingPolicy) {
		if(maxNumberOfAllowedOpenStream<=0)
			throw new IllegalArgumentException("Cannot manage less than 1 OutputStream.");
		this.openStreamCount = 0;
		this.closingPolicy = closingPolicy;
		this.maxNumberOfAllowedOpenStream = maxNumberOfAllowedOpenStream;
		this.notDefinitelyCloseStream = new HashSet<ManagedOutputStream<? extends OutputStream>>();
	}

	/**
	 * Method that set the max number of opened stream at same time.
	 * @param maxNumberOfAllowedOpenStream The max number of opened stream at same time
	 */
	public synchronized void setMaxNumberOfOpenStreamAllowed(int maxNumberOfAllowedOpenStream) {
		if(maxNumberOfAllowedOpenStream < 1)
			throw new IllegalArgumentException("You need to open at least one file!");
		if(this.openStreamCount > maxNumberOfAllowedOpenStream)
			throw new IllegalArgumentException("Cannot set a number less than current opened file!");
		this.maxNumberOfAllowedOpenStream = maxNumberOfAllowedOpenStream;
	}

	/**
	 * Methods that check the number of opened stream and if necessary close one stream.
	 * @throws IOException If raised an {@link IOException}
	 */
	private void checkNumberOfStreamOpened() throws IOException {
		if(this.canOpenStream())
			return; //nothing to do
		//must close another stream
		ManagedOutputStream<? extends OutputStream> streamToClose = this.closingPolicy.getStreamToClose();
		streamToClose.temporaryClosure();
	}
	/**
	 * Method that return <code>true</code> the number of opened stream is less than maximum, 
	 * <code>false</code> otherwise.
	 * @return <code>true</code> the number of opened stream is less than maximum <code>false</code> otherwise
	 */
	private boolean canOpenStream() {
		return this.openStreamCount  < this.maxNumberOfAllowedOpenStream;
	}

	/**
	 * Methods to notify the temporary closure to a {@link ManagedOutputStream}.
	 * @param stream The stream temporary closed
	 */
	synchronized void notifyTemporaryClosure(ManagedOutputStream<? extends OutputStream> stream) {
		this.openStreamCount--;
		this.closingPolicy.notifyTemporaryClosed(stream);
	}
	/**
	 * Methods to notify the definitively closure to a {@link ManagedOutputStream}.
	 * @param stream The stream definitively closed
	 */
	synchronized void notifyDefinitevelyClosure(ManagedOutputStream<? extends OutputStream> stream) {
		this.openStreamCount--;
		this.closingPolicy.notifyDefinitelyClosed(stream);
		this.notDefinitelyCloseStream.remove(stream);
	}
	/**
	 * Methods to notify a new {@link ManagedOutputStream}.
	 * @param newStream The new stream
	 * @throws IOException If raised an {@link IOException}
	 */
	synchronized void notifyNewManagedOutputStream(ManagedOutputStream<? extends OutputStream> newStream) throws IOException {
		this.checkNumberOfStreamOpened();
		this.openStreamCount++;
		this.closingPolicy.notifyNewManagedOutputStream(newStream);
		this.notDefinitelyCloseStream.add(newStream);
	}
	/**
	 * Methods to notify the reopen of a {@link ManagedOutputStream}.
	 * @param stream The stream reopened
	 * @throws IOException If raised an {@link IOException}
	 */
	synchronized void notifyReOpened(ManagedOutputStream<? extends OutputStream> stream) throws IOException {
		this.checkNumberOfStreamOpened();
		this.openStreamCount++;
		this.closingPolicy.notifyOpen(stream);
	}
	
	/**
	 * Method to close all the {@link ManagedOutputStream}.
	 */
	public void closeAll() {
		for(ManagedOutputStream<? extends OutputStream> stream : new HashSet<ManagedOutputStream<? extends OutputStream>>(this.notDefinitelyCloseStream)) {
			try {
				stream.flush();
				stream.close();	//definitely close
			} catch (IOException e) {/* skip closure */}
		}
	}
	/**
	 * Method that add an hook for close all {@link ManagedOutputStream} stream at JVM shutdown.
	 */
	public void addHookforClosure() {
		Runtime.getRuntime().addShutdownHook(new Thread() {
				@Override
				public void run() {
					OutputStreamManager.this.closeAll();
				}
			}
		);
	}
	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#finalize()
	 */
	@Override
	protected void finalize() throws Throwable {
		this.closeAll();
	}
		
}
